import { SQL, SQLQuery } from '@databases/sql'
import { FastifyError } from '@fastify/error'
import { fastify, FastifyInstance, FastifyReply } from 'fastify'
import { expectType } from 'tsd'
import {
  connect,
  createConnectionPool,
  Database,
  DBEntityField,
  Entities,
  Entity,
  EntityHooks,
  errors,
  PlatformaticContext,
  plugin,
  SQLMapperPluginInterface,
  utils,
  WhereClause
} from '../../index.js'

const log = {
  trace () {},
  error () {},
  warn () {}
}

declare module 'fastify' {
  interface FastifyInstance {
    platformatic: SQLMapperPluginInterface<Entities>
  }
}

const pluginOptions: SQLMapperPluginInterface<Entities> = await connect<Entities>({ connectionString: '', log })
expectType<Database>(pluginOptions.db)
expectType<SQL>(pluginOptions.sql)
expectType<{ [entityName: string]: Entity }>(pluginOptions.entities)

expectType<Promise<void>>(pluginOptions.cleanUpAllEntities())

expectType<SQLMapperPluginInterface<Entities>>(
  await connect<Entities>({
    connectionString: '',
    poolSize: 2,
    queueTimeoutMilliseconds: 42,
    idleTimeoutMilliseconds: 42,
    acquireLockTimeoutMilliseconds: 42
  })
)

expectType<SQLMapperPluginInterface<Entities>>(
  await connect<Entities>({
    connectionString: '',
    cache: false
  })
)

expectType<SQLMapperPluginInterface<Entities>>(
  await connect<Entities>({
    connectionString: '',
    cache: true
  })
)

expectType<SQLMapperPluginInterface<Entities>>(
  await connect<Entities>({
    connectionString: '',
    cache: {
      ttl: 1000,
      stale: 10
    }
  })
)

interface EntityFields {
  id: number
  name: string
}

const ctx: PlatformaticContext = { app: fastify(), reply: {} as FastifyReply }

const entity: Entity<EntityFields> = pluginOptions.entities.entityName
expectType<string>(entity.name)
expectType<string>(entity.singularName)
expectType<string>(entity.pluralName)
expectType<string>(entity.primaryKey)
expectType<Set<string>>(entity.primaryKeys)
expectType<string>(entity.table)
expectType<any[]>(entity.relations)
expectType<{ [columnName: string]: DBEntityField }>(entity.fields)
expectType<{ [columnName: string]: DBEntityField }>(entity.camelCasedFields)
expectType<(input: { [columnName: string]: any }) => { [columnName: string]: any }>(entity.fixInput)
expectType<(input: { [columnName: string]: any }) => { [columnName: string]: any }>(entity.fixOutput)
expectType<Partial<EntityFields>[]>(await entity.find())
expectType<Partial<EntityFields>[]>(await entity.insert({ inputs: [{ id: 1, name: 'test' }] }))
expectType<Partial<EntityFields>>(await entity.save({ input: { id: 1, name: 'test' } }))
expectType<Partial<EntityFields>[]>(await entity.delete())
expectType<number>(await entity.count())
expectType<Partial<EntityFields>[]>(await entity.find({ tx: pluginOptions.db }))
expectType<Partial<EntityFields>[]>(await entity.find({ ctx }))
expectType<Partial<EntityFields>[]>(await entity.insert({ inputs: [{ id: 1, name: 'test' }], tx: pluginOptions.db }))
expectType<Partial<EntityFields>>(await entity.save({ input: { id: 1, name: 'test' }, tx: pluginOptions.db }))
expectType<Partial<EntityFields>>(await entity.save({ input: { id: 1, name: 'test' }, ctx }))
expectType<Partial<EntityFields>[]>(await entity.delete({ tx: pluginOptions.db }))
expectType<Partial<EntityFields>[]>(await entity.delete({ ctx }))
expectType<number>(await entity.count({ tx: pluginOptions.db }))
expectType<number>(await entity.count({ ctx }))
expectType<Partial<EntityFields>[]>(
  await entity.updateMany({ where: { id: { eq: '1' } }, input: { id: 1, name: 'test' } })
)
expectType<Partial<EntityFields>[]>(
  await entity.updateMany({ where: { id: { eq: '1' } }, input: { id: 1, name: 'test' }, ctx })
)
expectType<Partial<EntityFields>[]>(await entity.updateMany({ where: { id: { eq: '1' } }, input: { name: 'test' } }))
expectType<Partial<EntityFields>[]>(await entity.find({ where: { id: { eq: null } } }))
expectType<Partial<EntityFields>[]>(await entity.find({ where: { id: { neq: null } } }))

const whereCondition: WhereClause = {
  eq: { eq: '' },
  eqNumber: { eq: 1 },
  eqBoolean: { eq: true },
  neq: { neq: '' },
  neqNumber: { neq: 1 },
  neqBoolean: { neq: true },
  gt: { gt: '' },
  gte: { gte: '' },
  lt: { lt: '' },
  lte: { lte: '' },
  in: { in: [] },
  nin: { nin: [] },
  like: { like: '' },
  ilike: { ilike: '' },
  all: { all: '' },
  any: { any: '' },
  contains: { contains: [] },
  contained: { contained: [] },
  overlaps: { overlaps: [] },
  or: [{ field: { eq: '' } }, { field: { eq: null } }]
}

await entity.find({ where: whereCondition })
await entity.delete({ where: whereCondition })
await entity.count({ where: whereCondition })
await entity.updateMany({ where: whereCondition, input: { id: 1, name: 'test' } })

const entityHooks: EntityHooks = {
  async find (
    originalFind: typeof entity.find,
    ...options: Parameters<typeof entity.find>
  ): ReturnType<typeof entity.find> {
    return []
  },
  async insert (
    originalInsert: typeof entity.insert,
    ...options: Parameters<typeof entity.insert>
  ): ReturnType<typeof entity.insert> {
    return []
  },
  async save (
    originalSave: typeof entity.save,
    ...options: Parameters<typeof entity.save>
  ): ReturnType<typeof entity.save> {
    return {}
  },
  async delete (
    originalDelete: typeof entity.delete,
    ...options: Parameters<typeof entity.delete>
  ): ReturnType<typeof entity.delete> {
    return []
  },
  async count (
    originalCount: typeof entity.count,
    ...options: Parameters<typeof entity.count>
  ): ReturnType<typeof entity.count> {
    return 0
  },
  async updateMany (
    originalUpdateMany: typeof entity.updateMany,
    ...options: Parameters<typeof entity.updateMany>
  ): ReturnType<typeof entity.updateMany> {
    return []
  }
}
expectType<EntityHooks>(entityHooks)
expectType<SQLMapperPluginInterface<Entities>>(await connect<Entities>({ connectionString: '', log }))
expectType<SQLMapperPluginInterface<Entities>>(
  await connect<Entities>({ connectionString: '', autoTimestamp: true, log })
)
expectType<SQLMapperPluginInterface<Entities>>(await connect<Entities>({ connectionString: '', hooks: {}, log }))
expectType<SQLMapperPluginInterface<Entities>>(
  await connect<Entities>({
    connectionString: '',
    hooks: {
      Page: entityHooks
    },
    log
  })
)
expectType<SQLMapperPluginInterface<Entities>>(await connect<Entities>({ connectionString: '', ignore: {}, log }))
expectType<SQLMapperPluginInterface<Entities>>(
  await connect<Entities>({
    connectionString: '',
    log,
    onDatabaseLoad (db: Database, sql: SQL) {
      expectType<(query: SQLQuery) => Promise<any[]>>(db.query)
      expectType<() => Promise<void>>(db.dispose)
      expectType<(fn: (tx: Database) => Promise<EntityFields>, options?: any) => Promise<EntityFields>>(
        db.tx<EntityFields>
      )
      expectType<boolean | undefined>(pluginOptions.db.isMySql)
      expectType<boolean | undefined>(pluginOptions.db.isMariaDB)
      expectType<boolean | undefined>(pluginOptions.db.isSQLite)
      expectType<boolean | undefined>(pluginOptions.db.isPg)
    }
  })
)

const instance: FastifyInstance = fastify()
instance.register(plugin, { connectionString: '', autoTimestamp: true })
instance.register(instance => {
  expectType<SQLMapperPluginInterface<Entities>>(instance.platformatic)

  instance.platformatic.addEntityHooks<EntityFields>('something', {
    async find (originalFind, options) {
      expectType<Partial<EntityFields>[]>(await originalFind())
      expectType<Parameters<typeof entity.find>[0]>(options)

      return [
        {
          id: 42
        }
      ]
    }
  })

  instance.get('/', async (request, reply) => {
    const ctx = request.platformaticContext
    expectType<FastifyInstance>(ctx.app)
    expectType<FastifyReply>(ctx.reply)
    await instance.platformatic.cleanUpAllEntities()
  })
})

expectType<(str: string) => string>(utils.toSingular)

expectType<Promise<{ db: Database; sql: SQL }>>(createConnectionPool({ connectionString: '', log }))

// Errors
type ErrorWithNoParams = () => FastifyError
type ErrorWithOneParam = (param: string) => FastifyError
type ErrorWithOneAnyParam = (param: any) => FastifyError
type ErrorWithTwoParams = (param1: string, param2: string) => FastifyError

expectType<ErrorWithOneParam>(errors.CannotFindEntityError)
expectType<ErrorWithNoParams>(errors.SpecifyProtocolError)
expectType<ErrorWithNoParams>(errors.ConnectionStringRequiredError)
expectType<ErrorWithOneAnyParam>(errors.TableMustBeAStringError)
expectType<ErrorWithOneParam>(errors.UnknownFieldError)
expectType<ErrorWithNoParams>(errors.InputNotProvidedError)
expectType<ErrorWithOneParam>(errors.UnsupportedWhereClauseError)
expectType<ErrorWithNoParams>(errors.UnsupportedOperatorForArrayFieldError)
expectType<ErrorWithNoParams>(errors.UnsupportedOperatorForNonArrayFieldError)
expectType<ErrorWithOneParam>(errors.ParamNotAllowedError)
expectType<ErrorWithTwoParams>(errors.InvalidPrimaryKeyTypeError)
expectType<ErrorWithTwoParams>(errors.ParamLimitNotAllowedError)
expectType<ErrorWithOneParam>(errors.ParamLimitMustBeNotNegativeError)
expectType<ErrorWithOneParam>(errors.MissingValueForPrimaryKeyError)
expectType<ErrorWithNoParams>(errors.SQLiteOnlySupportsAutoIncrementOnOneColumnError)
